import datetime
import math
import numpy as np
import pandas as pd
import tabulate

from coin.proto.coin_request_pb2 import StrategyInfoProto
from coin.proto.coin_strategy_summary_pb2 import (StrategyStat, StrategySummaryMessage)
from coin.strategy.tool.monitor.log_parser_runner import (_format_timedelta_long,
                                                          format_timedelta_short)
from coin.base.query_util import query_exchange_rates
from xunkemgmt_client.tool.slack_noti import send_to_slack


def _ts_to_dt(ts):
  return None if ts is None else datetime.datetime.utcfromtimestamp(ts / 10**9)


def _get_ret_bp(pnl, turnover):
  ret_bp = None
  if pnl is not None:
    if turnover is not None and turnover > 0 and not math.isclose(turnover, 0, abs_tol=1e-9):
      ret_bp = pnl / turnover * 10000
  return ret_bp


def _strat_stat_to_list(strat_info, account_request, symbol, stat):
  assert isinstance(strat_info, StrategyInfoProto)
  assert isinstance(stat, StrategyStat)
  name = strat_info.strategy_name
  group = strat_info.strategy_group
  trader = strat_info.trader
  account = account_request.owner if account_request is not None else None
  pnl_mark = stat.pnl_mark if stat.HasField('pnl_mark') else None
  pnl_4hr = stat.pnl_4hr if stat.HasField('pnl_4hr') else None
  pnl_8hr = stat.pnl_8hr if stat.HasField('pnl_8hr') else None
  pnl_mark_in_usd = \
    stat.pnl_mark_in_usd if stat.HasField('pnl_mark_in_usd') else None
  pnl_mark_from_fill = stat.pnl_mark_from_fill if \
                       stat.HasField('pnl_mark_from_fill') else None
  pnl_mark_from_fill_in_usd = stat.pnl_mark_from_fill_in_usd if \
                           stat.HasField('pnl_mark_from_fill_in_usd') else None
  active_trading_pnl_mark = stat.active_trading_pnl_mark if \
                           stat.HasField('active_trading_pnl_mark') else None
  active_trading_pnl_mark_in_usd = stat.active_trading_pnl_mark_in_usd if \
                           stat.HasField('active_trading_pnl_mark_in_usd') else None
  sharpe_ratio = stat.sharpe_ratio if stat.HasField('sharpe_ratio') else None
  fee_mark = stat.fee_mark if stat.HasField('fee_mark') else None
  turnover_mark = stat.turnover_mark if stat.HasField('turnover_mark') else None
  turnover_4hr = stat.turnover_4hr if stat.HasField('turnover_4hr') else None
  turnover_8hr = stat.turnover_8hr if stat.HasField('turnover_8hr') else None
  ret_bp_mark = _get_ret_bp(pnl_mark, turnover_mark)
  fill_ratio_mark = \
    stat.fill_ratio_mark if stat.HasField('fill_ratio_mark') else None
  fill_ratio_4hr = \
    stat.fill_ratio_4hr if stat.HasField('fill_ratio_4hr') else None
  fill_ratio_8hr = \
    stat.fill_ratio_8hr if stat.HasField('fill_ratio_8hr') else None
  full_fill_ratio_8hr = \
    stat.full_fill_ratio_8hr if stat.HasField('full_fill_ratio_8hr') else None
  partial_fill_ratio_8hr = stat.partial_fill_ratio_8hr if \
                           stat.HasField('partial_fill_ratio_8hr') else None
  agg_ratio_qty_mark = stat.agg_ratio_qty_mark if \
                       stat.HasField('agg_ratio_qty_mark') else None
  agg_ratio_qty_inferred_mark = \
    stat.agg_ratio_qty_inferred_mark if \
    stat.HasField('agg_ratio_qty_inferred_mark') else None
  position = stat.position if stat.HasField('position') else None
  position_4hr = stat.avg_position_4hr if stat.HasField('avg_position_4hr') \
                 else None
  last_log_dt = _ts_to_dt(stat.last_log_ts if stat.HasField('last_log_ts') else None)
  last_fill_dt = _ts_to_dt(stat.last_fill_ts if stat.HasField('last_fill_ts') else None)

  return [
      trader,
      name,
      group,
      stat.accounting_currency,
      account,
      symbol,
      pnl_mark,
      pnl_4hr,
      pnl_8hr,
      pnl_mark_in_usd,
      pnl_mark_from_fill,
      pnl_mark_from_fill_in_usd,
      active_trading_pnl_mark,
      active_trading_pnl_mark_in_usd,
      sharpe_ratio,
      fee_mark,
      turnover_mark,
      turnover_4hr,
      turnover_8hr,
      ret_bp_mark,
      fill_ratio_mark,
      fill_ratio_4hr,
      fill_ratio_8hr,
      full_fill_ratio_8hr,
      partial_fill_ratio_8hr,
      agg_ratio_qty_mark,
      agg_ratio_qty_inferred_mark,
      position,
      position_4hr,
      last_fill_dt,
      last_log_dt,
  ]


def _strategy_summary_message_proto_to_df(result):
  header = [
      'trader',
      'strategy_name',
      'strategy_group',
      'quote',
      'account',
      'symbol',
      'pnl_mark',
      'pnl_4hr',
      'pnl_8hr',
      'pnl_mark_in_usd',
      'pnl_mark_from_fill',
      'pnl_mark_from_fill_in_usd',
      'active_trading_pnl_mark',
      'active_trading_pnl_mark_in_usd',
      'sharpe_ratio',
      'fee_est',
      'turnover_mark',
      'turnover_4hr',
      'turnover_8hr',
      'ret_bp_mark',
      'FR%_mark',
      'FR%_4hr',
      'FR%_8hr',
      'FFR%_8hr',
      'PFR%_8hr',
      'AGGR%',
      'AGGR_inf%',
      'position',
      'position_4hr',
      'last_fill',
      'last_log',
  ]
  elems = []
  assert isinstance(result, StrategySummaryMessage)
  for summary in result.summaries:
    info = summary.strategy
    for quote, stat in summary.stat_per_quote.items():
      elems.append(_strat_stat_to_list(info, None, None, stat))
    for account_id, account_stat in summary.stat_per_account.items():
      account_request = account_stat.account_request
      for symbol, stat in account_stat.stat_per_symbol.items():
        elems.append(_strat_stat_to_list(info, account_request, symbol, stat))
  df = pd.DataFrame(elems, columns=header)
  return df


def _convert_to_multirow(df):
  df = df.sort_values(by=['trader', 'strategy_name', 'account', 'symbol'])
  df = df.fillna('-')
  df = df.replace('', '-')
  df['symbol'][df['symbol'] == '-'] = 'Total'
  converted_df = pd.DataFrame(columns=df.columns.tolist())
  for _, row in df.iterrows():
    if len(converted_df) == 0:
      converted_df = converted_df.append(row)
    else:
      last_row = converted_df.iloc[-1]
      if last_row['strategy_name'] == row['strategy_name']:
        for index, value in row.iteritems():
          try:
            if index not in ['strategy_name', 'trader']:
              last_row[index] += '\n%s' % value
          except Exception:
            pass
      else:
        converted_df = converted_df.append(row)
  return converted_df


def _report(df, mode):
  columns = [
      'trader',
      'strategy_name',
      'account',
      'symbol',
      'position',
      'position_4hr',
      'pnl_mark',
      'pnl_mark_in_usd',
      'pnl_mark_from_fill',
      'active_trading_pnl_mark',
      'sharpe_ratio',
      'fee_est',
      'pnl_4hr',
      'turnover_mark',
      'turnover_4hr',
      'ret_bp_mark',
      'FR%_mark',
      'FR%_8hr',
      'FFR%_8hr',
      'PFR%_8hr',
      'AGGR%',
      'AGGR_inf%',
      'last_fill',
      'last_log',
  ]
  df = df[columns]

  float_fmt = lambda x: '' if pd.isnull(x) else "{:,.3f}".format(x)
  turnover_fmt = lambda x: '' if (pd.isnull(x) or math.isclose(x, 0, abs_tol=1e-9)) else "{:,.3f}".format(x)
  percentage_fmt = lambda x: '' if pd.isnull(x) else '%.1f' % (x * 100)
  formatters = {
      'position': float_fmt,
      'position_4hr': float_fmt,
      'pnl_mark': float_fmt,
      'pnl_4hr': float_fmt,
      'pnl_mark_in_usd': float_fmt,
      'pnl_mark_from_fill': float_fmt,
      'active_trading_pnl_mark': float_fmt,
      'sharpe_ratio': float_fmt,
      'fee_est': float_fmt,
      'turnover_mark': turnover_fmt,
      'turnover_4hr': turnover_fmt,
      'ret_bp_mark': float_fmt,
      'FR%_mark': percentage_fmt,
      'FR%_8hr': percentage_fmt,
      'FFR%_8hr': percentage_fmt,
      'PFR%_8hr': percentage_fmt,
      'AGGR%': percentage_fmt,
      'AGGR_inf%': percentage_fmt,
  }
  df_print = df.copy()
  for col, formatter in formatters.items():
    df_print[col] = df_print[col].apply(formatter)

  if mode == 'monitor':
    table_columns = columns
    total_columns = ['pnl_mark', 'turnover_mark', 'pnl_mark_in_usd']
    tablefmt = 'rst'
    stralign = 'right'
    df_print = _convert_to_multirow(df_print)
  elif mode == 'report':
    table_columns = [
      'trader', 'strategy_name', 'pnl_mark',
      'pnl_mark_from_fill', 'active_trading_pnl_mark', 'turnover_mark']
    total_columns = [
      'pnl_mark', 'turnover_mark', 'pnl_mark_from_fill', 'active_trading_pnl_mark']
    tablefmt = 'simple'
    stralign = 'left'
    df_print = df[pd.isnull(df['symbol'])].sort_values(by=['strategy_name', 'symbol'])
  else:
    raise ValueError(mode)

  df_print.fillna('', inplace=True)
  report_string = tabulate.tabulate(df_print[table_columns],
                                    showindex=False,
                                    headers=table_columns,
                                    floatfmt='.3f',
                                    stralign=stralign,
                                    tablefmt=tablefmt)
  for column in total_columns:
    total = df[pd.isnull(df['symbol'])][column].sum(min_count=1)
    report_string += ('\n * Total %-26s %s' % ('%s:' % column,
                                               '' if pd.isnull(total) else "{:26,.03f}".format(total)))
  return report_string


def print_strategy_summary(result, mode='monitor', ticker=None, print_summary=True):
  assert mode in ['monitor', 'report']
  source_infos = set([
      summary.source.strategy_log_info.source_info for summary in result.summaries])
  strategy_summary_string = f'source_info: {", ".join(sorted(list(source_infos)))}\n'
  df = _strategy_summary_message_proto_to_df(result)
  if len(df) == 0:
    return
  fetched_dt = _ts_to_dt(result.fetched_ts)
  time_diff_fmt = lambda x: '' if pd.isnull(x) else \
                            _format_timedelta_long(fetched_dt - x)
  short_time_fmt = lambda x: '%s' % (format_timedelta_short(fetched_dt - x))
  df['last_log'] = df['last_log'].apply(time_diff_fmt)
  df['last_fill'] = df['last_fill'].apply(time_diff_fmt)
  df = df.fillna(np.nan)

  strategy_summary_string += 'fetched time: %s\n' % fetched_dt.strftime('%Y%m%d-%H%M%S')

  float_fmt = lambda x: '' if pd.isnull(x) else "{:,.3f}".format(x)
  strategy_summary_string += '\n%s\n' % df.groupby('strategy_group').sum()[
      ['pnl_mark_in_usd']].to_string(float_format=float_fmt)

  for (strategy_group, quote), group in df.groupby(['strategy_group', 'quote']):
    strategy_summary_string += '\nStrategy group: %s, Pnl currency: %s\n' % (
        strategy_group, quote)
    strategy_summary_string += '%s\n' % _report(group, mode)

  df = df[df['symbol'].isnull()]
  df = df[[
    'quote', 'pnl_mark', 'turnover_mark',
    'pnl_mark_from_fill_in_usd', 'active_trading_pnl_mark_in_usd']].groupby('quote').sum(min_count=1)
  base_list = df.index.tolist()
  quote = 'USD'
  if ticker is None:
    df['pnl_price_in_usd'] = query_exchange_rates(base_list, quote)
  else:
    df['pnl_price_in_usd'] = [ticker.get('%s-%s' % (base, quote), None) for base in base_list]

  df['pnl_in_usd'] = df['pnl_mark'] * df['pnl_price_in_usd']
  df['turnover_mark_in_usd'] = df['turnover_mark'] * df['pnl_price_in_usd']
  df.loc['Total'] = df.sum(axis=0, min_count=1)
  df.loc['Total', ['pnl_mark', 'turnover_mark', 'pnl_price_in_usd']] = np.nan
  df = df[['pnl_mark', 'turnover_mark',  'pnl_price_in_usd', 'pnl_in_usd',
           'pnl_mark_from_fill_in_usd', 'active_trading_pnl_mark_in_usd', 'turnover_mark_in_usd']]
  strategy_summary_string += '\nTotal pnl_mark:\n %s' % df.to_string(
      float_format=float_fmt, index_names=False)
  if print_summary:
    print(strategy_summary_string)
  return strategy_summary_string


def _formatNumber(number):
  if isinstance(number, int) or isinstance(number, float):
    return '{:,.3f}'.format(number)
  else:
    return number


def _formatDict(d: dict):
  for key, value in d.items():
    if isinstance(value, dict):
      _formatDict(value)
    else:
      d[key] = _formatNumber(d[key])
  return d


def convert_balance_checker_summary_for_display(result, exchange_rate):
  balance_extra = pd.DataFrame(result)
  display_df = pd.DataFrame()
  columns_order_list = [
      'strat_name',
      'market_type',
      'exchange',
      'owner',
      'config',
      'price',
      'max_pq',
      'quote',
      'actual',
      'required',
      'extra',
      'extra_pct',
      'extra_in_USD'
  ]
  for i in balance_extra.index:
    strat_df_value = []
    strat_cur_balance = balance_extra.loc[i, 'cur_balance']
    strat_min_balance = balance_extra.loc[i, 'min_balance']
    strat_extra_balance = balance_extra.loc[i, 'extra_balance']
    for currency in strat_min_balance.keys():
      currency_balance = strat_cur_balance[currency]
      currency_min_balance = strat_min_balance[currency]
      currency_extra_balance = strat_extra_balance[currency]
      extra_balance_by_USD = currency_extra_balance * exchange_rate[currency]
      market_type =  balance_extra.loc[i, 'market_type']
      if market_type == 'Spot':
        currency_config = balance_extra.loc[i, 'config']
        currency_price = balance_extra.loc[i, 'symbol_price']
      elif market_type == 'Futures':
        currency_config = balance_extra.loc[i, 'config_curr'].get(currency)
        currency_price = balance_extra.loc[i, 'price_curr'].get(currency)
      posdf = pd.DataFrame(currency_config).T
      prcseri = pd.Series(currency_price)
      posdf['price'] = prcseri
      posdf['max_pq'] = posdf['max_pos'] * posdf['price']
      strat_df_value.append({
          'strat_name': balance_extra.loc[i, 'strat_name'],
          'market_type': market_type,
          'exchange': balance_extra.loc[i, 'exchange'],
          'owner': balance_extra.loc[i, 'owner'],
          'config':_formatDict(currency_config),
          'price':_formatDict(currency_price),
          'max_pq':_formatDict(posdf['max_pq'].to_dict()),
          'quote': currency,
          'actual': _formatNumber(currency_balance),
          'required': _formatNumber(currency_min_balance),
          'extra': _formatNumber(currency_extra_balance),
          'extra_pct':
              '%.2f%%' % (currency_extra_balance / currency_min_balance * 100)
              if currency_min_balance != 0 else 'NaN',
          'extra_in_USD':
              extra_balance_by_USD,
      })
    if strat_df_value:
      strat_df = pd.DataFrame(strat_df_value)
      strat_df = strat_df[columns_order_list]
      display_df = display_df.append(strat_df)
  display_df.sort_values('extra_in_USD', inplace=True)
  display_df['extra_in_USD'] = display_df['extra_in_USD'].apply(_formatNumber)
  return display_df


def print_balance_checker_summary(display_df):
  display_list = []
  display_df.index = range(len(display_df))
  for i in display_df.index:
    if len(display_df.loc[i, 'config']) > 1:
      strat_extra_balance_list = display_df.loc[i].tolist()
      strat_extra_balance_list[4] = ''
      strat_extra_balance_list[5] = ''
      strat_extra_balance_list[6] = ''
      display_list.append(strat_extra_balance_list)
      for symbol in display_df.loc[i, 'config'].keys():
        symbol_extra_balance_list = [''] * 4
        symbol_extra_balance_list.append({symbol: display_df.loc[i, 'config'][symbol]})
        symbol_extra_balance_list.append({symbol: display_df.loc[i, 'price'].get(symbol)})
        symbol_extra_balance_list.append({symbol: display_df.loc[i, 'max_pq'].get(symbol)})
        symbol_extra_balance_list.extend([''] * 6)
        display_list.append(symbol_extra_balance_list)
    else:
      display_list.append(display_df.loc[i].tolist())
  print(pd.DataFrame(display_list, columns=display_df.columns).to_string())


def send_strat_summary_to_slack(slack_receiver, strat_summary, prefix_msg=''):
  if slack_receiver is not None:
    send_to_slack(strat_summary, slack_receiver, 'file')
    total_summary = strat_summary.split("Total pnl_mark:")[-1]
    total_summary = prefix_msg + total_summary
    send_to_slack(total_summary, slack_receiver, 'file')
